// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef LIBV_LSFILE_H_
#define LIBV_LSFILE_H_

#include <errno.h>
#include <libv/old_sfile.h>

#if defined(_WIN32)
#include <libv/windows_lean.h>
# include <limits>           // numeric_limits::max(), numeric_limits::min()
#else
# include <sys/file.h>       // flock
# define _FILE_OFFSET_BITS 64
# ifndef _XOPEN_SOURCE
#  define _XOPEN_SOURCE 600  // fallocate
# endif
#endif  // _WIN32



namespace libv
{

/** Класс-декоратор над SFILE, добавляющий блокировки файлов.
  * Запрещается использовать совместно с SFILE: при использовании одного файла
  * должна использоваться или неблокирующая логика или блокирующая реализация.
  *
  * Изначально тип системной блокировки файлов платформозависим.
  * В Linux: рекомендательная (advisory), никакие действия не запрещены. \n
  * В Windows: обязательная (mandatory), то есть:
  *  - при установке разделяемой блокировки на файл, запись в него запрещена
  *    всем процессам, даже заблокировавшему
  *  - при установке эксклюзивной блокировки на файл, всем процессам кроме
  *    заблокировавшего запрещены любые действия с файлом.
  *
  * Класс реализован таким образом, что при работе с файлами только через
  * LSFILE, блокировки становятся обязательными с логикой Windows.
  *
  * На файл для записи при открытии устанавливается эксклюзивная блокировка,
  * для чтения — разделяемая.
  *
  * Не гарантируется, что блокировка файла передается детям (через fork(2)).
  * Блокировка заблокированного текущим процессом файла через другой
  * дескриптор этого процесса попадает в общую очередь ожидания.
  * В Linux: блокировка передается и при замене процесса через execve. \n
  * В данной реализации процесс может держать на файле только один из типов
  * блокировок. Последующие вызовы другого типа блокировки будут
  * неатомарно(!) преобразовывать текущую блокировку к новому типу
  * (семантика Linux flock(2)).\n
  * При закрытии файла блокировка на него снимается автоматически, однако,
  * в случае возникновения ошибки, ее нельзя уже будет обработать. \n
  * Проверка на дедлоки не производится.
  *
  * @note Из-за странной семантики fcntl, связанной с закрытием одного из
  * двух дескрипторов одного и того же файла в одном процессе, блокировки
  * участков файла не реализованы (см. man 2 fcntl).
  *
  * @note Карта ошибок:
  *  - 0 успешное выполнение функции
  *  - -1 ошибки в вызове SFILE-объекта
  *  - -2 аргументы не соблюдают предусловие
  *  - -3 ошибки при взятии/освобождении блокировки (ошибка в LSFILE)
  *  - -4 неуспешная попытка взятия блокировки (только в Try... методах).
  */
class LSFILE
{
  public:
    typedef SFILE::PreallocType PreallocType;

    // Структура нужна, чтобы typedef'ом передать настройки обертке.
    struct LockType
    {
        static const int kInvalidLock = 0;
        static const int kNoLock = 1;
        static const int kSharedLock = 2;
        static const int kExclusiveLock = 3;

        static bool is_valid(const int lock_type)
        {
            return lock_type == kNoLock ||
                   lock_type == kSharedLock ||
                   lock_type == kExclusiveLock;
        }
    };

    /** Инициализация полей класса. */
    inline LSFILE();

    /** Деструктор, вызывающий для открытого файла Close() без проверки ошибок.
      * @see Close(). */
    inline ~LSFILE();

    /** Вызов SFILE::Create() и попытка эксклюзивной блокировки файла.
      * @post lock_type_() == kExclusiveLock.
      * @return результат SFILE::Create().
      * @retval 0 успешное выполнение
      * @retval -3 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -4 не удалось получить блокировку на файл, объект обнулен.
      * @note В случае ошибки, класс переводится в обнуленное состояние. */
    inline int Create(const ::std::string& filepath, const int mode = 0600);

    /** Вызов SFILE::Open() и попытка эксклюзивной блокировки файла.
      * @post lock_type_() == kExclusiveLock.
      * @return результат SFILE::Open().
      * @retval 0 успешное выполнение
      * @retval -3 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -4 не удалось получить блокировку на файл, объект обнулен.
      * @note В случае ошибки, класс переводится в обнуленное состояние. */
    inline int Open(const ::std::string& filepath);

    /** Вызов SFILE::Open() const и попытка разделяемой блокировка файла.
      * @post lock_type_() == kSharedLock.
      * @return результат SFILE::Open() const.
      * @retval 0 успешное выполнение
      * @retval -3 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -4 не удалось получить блокировку на файл, объект обнулен.
      * @note В случае ошибки, класс переводится в обнуленное состояние. */
    inline int Open(const ::std::string& filepath) const;

    /** Вызов SFILE::is_opened(). */
    inline bool is_opened() const;

    /** Вызов SFILE::Resize().
      * @pre lock_type() == kExclusiveLock.
      * @return результат SFILE::Resize().
      * @retval -2 аргументы не соблюдают предусловие. */
    inline int Resize(const fpos64 max_off);

    /** Разблокировка файла (если есть) и вызов SFILE::Close().
      * @post lock_type() == kNoLock.
      * @return результат SFILE::Close(). */
    inline int Close();

    /** Разблокировка файла (если есть) и вызов SFILE::Close() const.
      * @post lock_type() == kNoLock.
      * @return результат SFILE::Close(). */
    inline int Close() const;

    /** Вызов SFILE::Write().
      * @pre lock_type() == kExclusiveLock.
      * @return результат SFILE::Write().
      * @retval -2 аргументы не соблюдают предусловие. */
    inline int Write(fpos64* offset, const void* data, const size_t data_size);

    /** Вызов SFILE::Read() const.
      * @pre lock_type() == kExclusiveLock || lock_type() == kSharedLock.
      * @return результат SFILE::Read().
      * @retval -2 аргументы не соблюдают предусловие. */
    inline int Read(fpos64* offset,
                    void* buffer, const size_t buffer_size) const;

    /** Вызов SFILE::Size() const.
      * @pre lock_type() == kExclusiveLock || lock_type() == kSharedLock.
      * @return результат SFILE::Size().
      * @retval -2 аргументы не соблюдают предусловие. */
    inline fpos64 Size() const;

    /** Вызов SFILE::Flush().
      * @return результат SFILE::Flush(). */
    inline int Flush();

    /** Вызов SFILE::SetAutoRemove().
      * @pre lock_type() == kExclusiveLock.
      * @retval -2 аргументы не соблюдают предусловие. */
    inline int SetAutoRemove(const bool on);

    /** Вызов SFILE::SetPreallocType().
      * @pre lock_type() == kExclusiveLock.
      * @return результат SFILE::SetPreallocType().
      * @retval -2 аргументы не соблюдают предусловие. */
    inline int SetPreallocType(const int prealloc_type, const fsize p_param);

    /** Неблокирующий запрос разделяемой блокировки всего файла.
      * Ничего не делает, если объект имеет разделяемую блокировку над файлом.
      * Неатомарно попробует сменить на разделяемую,
      * если объект имеет эксклюзивную.
      * @pre is_opened() == true.
      * @post IsLocked() == true.
      * @retval 0 успешное выполнение
      * @retval -2 аргументы не соблюдают предусловие
      * @retval -3 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -4 блокировку взять не удалось. */
    inline int TrySharedLock() const;

    /** Неблокирующий запрос эксклюзивной блокировки всего файла.
      * Ничего не делает, если объект имеет эксклюзивную блокировку над файлом.
      * Неатомарно попробует сменить на эксклюзивную,
      * если объект имеет разделяемую.
      * @pre is_opened() == true.
      * @post IsLocked() == true.
      * @retval 0 успешное выполнение
      * @retval -2 аргументы не соблюдают предусловие
      * @retval -3 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -4 блокировку взять не удалось. */
    inline int TryExclusiveLock() const;

    /** Блокирующий запрос разделяемой блокировки всего файла.
      * Ничего не делает, если объект имеет разделяемую блокировку над файлом.
      * Неатомарно сменит на разделяемую, если объект имеет эксклюзивную.
      * @pre is_opened() == true.
      * @post IsLocked() == true.
      * @retval 0 успешное выполнение
      * @retval -2 аргументы не соблюдают предусловие
      * @retval -3 произошла внутренняя ошибка, last_error() вернет ее код. */
    inline int SharedLock() const;

    /** Блокирующий запрос эксклюзивной блокировки всего файла.
      * Ничего не делает, если объект имеет эксклюзивную блокировку над файлом.
      * Неатомарно сменит на эксклюзивную, если объект имеет разделяемую.
      * @pre is_opened() == true
      * @post IsLocked() == true.
      * @retval 0 успешное выполнение
      * @retval -2 аргументы не соблюдают предусловие
      * @retval -3 произошла внутренняя ошибка, last_error() вернет ее код. */
    inline int ExclusiveLock() const;

    /** Разблокировка всего файла.
      * @pre IsLocked() == true.
      * @post IsLocked() == false
      * @retval 0 успешное выполнение
      * @retval -2 аргументы не соблюдают предусловие
      * @retval -3 произошла внутренняя ошибка, last_error() вернет ее код. */
    inline int Unlock() const;

    /** Тип текущей блокировки (одно из значений анонимного перечисления). */
    inline int lock_type() const;

    /** Получение кода последней ошибки в системных вызовах внутри методов.
      * Функцию следует вызывать, только после возврата методом кода -1. */
    inline int last_error() const;

    /** Получение хендла файла.
      * Использовать только для служебных целей (например, блокировок), но не в
      * обход методов класса. */
    inline FileHandle file_handle() const;

    /** Получение имени файла.
      * Если is_opened() == false возвращается пустая строка. */
    inline ::std::string file_path() const;


  private:
    /** Реализация файлового объекта. */
    SFILE sfile_;

    /** Тип текущей блокировки. */
    mutable int lock_type_;

    /** Код последней ошибки. */
    mutable int last_error_;
};

LSFILE::LSFILE()
    : sfile_(),
      lock_type_(LockType::kNoLock) {}


LSFILE::~LSFILE()
{
    Close();
}


int LSFILE::Create(const ::std::string& filepath, int mode /* = 0600 */)
{
    int r = 0;
    if ( lock_type() != LockType::kNoLock )
    {
        r = Unlock();
        if ( r != 0 ) return r;
    }

    r = sfile_.Create(filepath, mode);
    if ( r != 0 )
    {
        if ( r == -1 ) last_error_ = sfile_.last_error();
        return r;
    }

    r = TryExclusiveLock();
    if ( r != 0 )
    {
        sfile_.SetAutoRemove(true);
        sfile_.Close();
    }
    return r;
}


int LSFILE::Open(const ::std::string& filepath)
{
    int r = 0;
    if ( lock_type() != LockType::kNoLock )
    {
        r = Unlock();
        if ( r != 0 ) return r;
    }

    r = sfile_.Open(filepath);
    if ( r != 0 )
    {
        if ( r == -1 ) last_error_ = sfile_.last_error();
        return r;
    }

    r = TryExclusiveLock();
    if ( r != 0 ) sfile_.Close();
    return r;
}


int LSFILE::Open(const ::std::string& filepath) const
{
    int r = 0;
    if ( lock_type() != LockType::kNoLock )
    {
        r = Unlock();
        if ( r != 0 ) return r;
    }

    r = sfile_.Open(filepath);
    if ( r != 0 )
    {
        if ( r == -1 ) last_error_ = sfile_.last_error();
        return r;
    }

    r = TrySharedLock();
    if ( r != 0 ) sfile_.Close();
    return r;
}


bool LSFILE::is_opened() const
{
    return sfile_.is_opened();
}


int LSFILE::Resize(fpos64 max_off)
{
    if ( lock_type() != LockType::kExclusiveLock )
        return -2;

    int r = sfile_.Resize(max_off);
    if ( r == -1 ) last_error_ = sfile_.last_error();
    return r;
}


// обязательно нужно 2 const-перегрузки с одинаковым телом,
// дабы вызывался правильные SFILE::Close().
int LSFILE::Close()
{
    if ( lock_type() != LockType::kNoLock )
        Unlock();
    int r = sfile_.Close();
    if ( r == -1 ) last_error_ = sfile_.last_error();
    return r;
}


int LSFILE::Close() const
{
    if ( lock_type() != LockType::kNoLock )
        Unlock();
    int r = sfile_.Close();
    if ( r == -1 ) last_error_ = sfile_.last_error();
    return r;
}


int LSFILE::Write(fpos64* offset, const void* data, size_t data_size)
{
    if ( lock_type() != LockType::kExclusiveLock )
        return -2;
    int r = sfile_.Write(offset, data, data_size);
    if ( r == -1 ) last_error_ = sfile_.last_error();
    return r;
}


int LSFILE::Read(fpos64* offset, void* buffer, const size_t buffer_size) const
{
    if ( lock_type() != LockType::kSharedLock &&
         lock_type() != LockType::kExclusiveLock )
        return -2;

    int r = sfile_.Read(offset, buffer, buffer_size);
    if ( r == -1 ) last_error_ = sfile_.last_error();
    return r;
}


fpos64 LSFILE::Size() const
{
    if ( lock_type() != LockType::kSharedLock &&
         lock_type() != LockType::kExclusiveLock )
        return -2;
    fpos64 r = sfile_.Size();
    if ( r == -1 ) last_error_ = sfile_.last_error();
    return r;
}


int LSFILE::Flush()
{
    int r = sfile_.Flush();
    if ( r == -1 ) last_error_ = sfile_.last_error();
    return r;
}


int LSFILE::SetAutoRemove(bool on)
{
    if ( lock_type() != LockType::kExclusiveLock )
        return -2;
    sfile_.SetAutoRemove(on);
    return 0;
}


int LSFILE::SetPreallocType(int type, fsize p_param)
{
    if ( lock_type() != LockType::kExclusiveLock )
        return -2;
    int r = sfile_.SetPreallocType(type, p_param);
    if ( r == -1 ) last_error_ = sfile_.last_error();
    return r;
}


int LSFILE::last_error() const
{
    return last_error_;
}


int LSFILE::lock_type() const
{
    return lock_type_;
}


FileHandle LSFILE::file_handle() const
{
    return sfile_.file_handle();
}


::std::string LSFILE::file_path() const
{
    return sfile_.file_path();
}


#if defined(_WIN32)

int LSFILE::TrySharedLock() const
{
    if ( is_opened() == false )
        return -2;

    if ( lock_type_ == LockType::kSharedLock )
        return 0;

    // В Windows файл может иметь 2 блокировки, но исходя из семантики
    // данного класса, снимаем предыдущую блокировку, симулируя замену.
    // Если на файле две блокировки, нужно вызывать 2 раза Unlock.
    if ( lock_type_ == LockType::kExclusiveLock )
    {
        int r = Unlock();
        if ( r != 0 ) return r;
    }

    OVERLAPPED overlapped = { 0 };      // через нее передаем начальный оффсет

    // блокируем все возможные байты
    if ( LockFileEx(sfile_.file_handle(),
                    LOCKFILE_FAIL_IMMEDIATELY, 0,
                    ::std::numeric_limits<DWORD>::max(),  // num of bytes low
                    ::std::numeric_limits<DWORD>::max(),  // num of bytes high
                    &overlapped) == FALSE )
    {
        last_error_ = GetLastError();

        // ошибка в том, что блокриовку взять не удалось
        if ( GetLastError() == ERROR_LOCK_VIOLATION )
            return -4;
        return -3;
    }
    lock_type_ = LockType::kSharedLock;
    return 0;
}


int LSFILE::TryExclusiveLock() const
{
    if ( is_opened() == false )
        return -2;

    if ( lock_type_ == LockType::kExclusiveLock )
        return 0;

    // разлочить Shared блокировку после установления Exclusive нельзя
    if ( lock_type_ == LockType::kSharedLock )
    {
        int r = Unlock();
        if ( r != 0 ) return r;
    }

    OVERLAPPED overlapped = { 0 };

    if ( LockFileEx(sfile_.file_handle(),
                    LOCKFILE_FAIL_IMMEDIATELY | LOCKFILE_EXCLUSIVE_LOCK, 0,
                    ::std::numeric_limits<DWORD>::max(),  // num of bytes low
                    ::std::numeric_limits<DWORD>::max(),  // num of bytes high
                    &overlapped) == FALSE )
    {
        // ошибка в том, что блокриовку взять не удалось
        last_error_ = GetLastError();
        if ( GetLastError() == ERROR_LOCK_VIOLATION )
            return -4;
        return -3;
    }
    lock_type_ = LockType::kExclusiveLock;
    return 0;
}


int LSFILE::SharedLock() const
{
    if ( is_opened() == false )
        return -2;

    if ( lock_type_ == LockType::kSharedLock )
        return 0;

    if ( lock_type_ == LockType::kExclusiveLock )
    {
        int r = Unlock();
        if ( r != 0 ) return r;
    }

    OVERLAPPED overlapped = { 0 };

    if ( LockFileEx(sfile_.file_handle(),
                    0, 0,
                    ::std::numeric_limits<DWORD>::max(),  // num of bytes low
                    ::std::numeric_limits<DWORD>::max(),  // num of bytes high
                    &overlapped) == FALSE )
    {
        last_error_ = GetLastError();
        return -3;
    }
    lock_type_ = LockType::kSharedLock;
    return 0;
}


int LSFILE::ExclusiveLock() const
{
    if ( is_opened() == false )
        return -2;

    if ( lock_type_ == LockType::kExclusiveLock )
        return 0;

    if ( lock_type_ == LockType::kSharedLock )
    {
        int r = Unlock();
        if ( r != 0 ) return r;
    }

    OVERLAPPED overlapped = { 0 };

    if ( LockFileEx(sfile_.file_handle(),
                    LOCKFILE_EXCLUSIVE_LOCK, 0,
                    ::std::numeric_limits<DWORD>::max(),  // num of bytes low
                    ::std::numeric_limits<DWORD>::max(),  // num of bytes high
                    &overlapped) == FALSE )
    {
        last_error_ = GetLastError();
        return -3;
    }
    lock_type_ = LockType::kExclusiveLock;
    return 0;
}


int LSFILE::Unlock() const
{
    // В Windows, в отличие от Linux нельзя вызывать Unlock на
    // незаблокированный файл, не будем тратить системный вызов на это
    if ( lock_type() == LockType::kNoLock )
        return -2;

    OVERLAPPED overlapped = { 0 };

    if ( UnlockFileEx(sfile_.file_handle(), 0,
                      ::std::numeric_limits<DWORD>::max(),  // num of bytes low
                      ::std::numeric_limits<DWORD>::max(),  // num of bytes high
                      &overlapped) == FALSE )
    {
        last_error_ = GetLastError();
        return -3;
    }
    lock_type_ = LockType::kNoLock;
    return 0;
}


#else

int LSFILE::TrySharedLock() const
{
    if ( is_opened() == false )
        return -2;

    if ( lock_type_ == LockType::kSharedLock )
        return 0;

    // В Linux нельзя иметь одновременно 2 блокировки на файл,
    // вызов новой преобразует текущую
    if ( flock(sfile_.file_handle(), LOCK_SH | LOCK_NB) == -1 )
    {
        last_error_ = errno;

        if ( errno == EWOULDBLOCK )
            return -4;
        return -3;
    }
    lock_type_ = LockType::kSharedLock;
    return 0;
}


int LSFILE::TryExclusiveLock() const
{
    if ( is_opened() == false )
        return -2;

    if ( lock_type_ == LockType::kExclusiveLock )
        return 0;

    if ( flock(sfile_.file_handle(), LOCK_EX | LOCK_NB) == -1 )
    {
        last_error_ = errno;

        if ( errno == EWOULDBLOCK )
            return -4;
        return -3;
    }
    lock_type_ = LockType::kExclusiveLock;
    return 0;
}


int LSFILE::SharedLock() const
{
    if ( is_opened() == false )
        return -2;

    if ( lock_type_ == LockType::kSharedLock )
        return 0;

    if ( flock(sfile_.file_handle(), LOCK_SH) == -1 )
    {
        last_error_ = errno;
        return -3;
    }
    lock_type_ = LockType::kSharedLock;
    return 0;
}


int LSFILE::ExclusiveLock() const
{
    if ( is_opened() == false )
        return -2;

    if ( lock_type_ == LockType::kExclusiveLock )
        return 0;

    if ( flock(sfile_.file_handle(), LOCK_EX) == -1 )
    {
        last_error_ = errno;
        return -3;
    }
    lock_type_ = LockType::kExclusiveLock;
    return 0;
}


int LSFILE::Unlock() const
{
    if ( lock_type() == LockType::kNoLock )
        return -2;

    if ( flock(sfile_.file_handle(), LOCK_UN) == -1 )
    {
        last_error_ = errno;
        return -3;
    }
    lock_type_ = LockType::kNoLock;
    return 0;
}

#endif  // _WIN32

}  // libv

#endif  // LIBV_LSFILE_H_
